﻿/*
 * Copyright 2011 Edward L. Stauff.                     Contact: <EdStauff@gmail.com>.
 * 
 * This file, "CmdLineParam.cs", is part of the "Stauffware.Common.Console" class 
 * library.  "Stauffware.Common" is free software: you can redistribute it and/or 
 * modify it under the terms of the GNU Lesser Public License as published by the 
 * Free Software Foundation, either version 3 of the License, or (at your option) 
 * any later version.
 * 
 * "Stauffware.Common" is distributed in the hope that it will be useful, 
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser Public License for more 
 * details.
 * 
 * You should have received a copy of the GNU Lesser Public License along with 
 * "Stauffware.Common".  If not, see <http://www.gnu.org/licenses/>.
 */

using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.Configuration;
using Stauffware.Common.Core;

namespace Stauffware.Common.Console
{
	/// <summary>
	/// Represents one command-line parameter.
	/// </summary>

	internal abstract class CmdLineParam : ICmdLineParam
	{
		#region Public Fields & Properties

		/// <summary>
		/// Gets the name of the parameter.
		/// </summary>
		public string Name { get; protected set; }

		/// <summary>
		/// If > 1, this specifies the position (relative to other positional 
		/// parameters) of this parameter's value, if its value is provided 
		/// without a name.  If Position = 0, then the parameter cannot be
		/// used as a positional parameter (that is, without its name).
		/// </summary>
		/// <remarks>
		/// Named parameters and their values are not counted when determining
		/// position.  The set of all positional parameters must have unique 
		/// Position values starting at 1 and monotonically increasing.
		/// </remarks>
		public int Position { get; protected set; }

		/// <summary>
		/// Gets whether this parameter can be positional; that is, can be
		/// used without its name.
		/// </summary>
		public bool CanBePositional { get { return this.Position > 0; } }

		/// <summary>
		/// Gets whether this parameter is a switch; that is, its name starts
		/// with the switch character.
		/// </summary>
		public bool IsSwitch 
		{ get { return this.Name.StartsWith(this.CmdLineParser.SwitchChar.ToString()); } }

		/// <summary>
		/// Gets or sets the help (documentation) for this parameter.
		/// </summary>
		public string Help { get; set; }
	
		/// <summary>
		/// Gets whether this parameter is required.
		/// </summary>
		public bool IsRequired { get; protected set; }

		/// <summary>
		/// Gets the parser to which this parameter belongs.
		/// </summary>
		public ICmdLineValueParser ValueParser { get; protected set; }

		/// <summary>
		/// Gets the number of values that the parameter can take: 0, 1, or N.
		/// (This is implied by the type of the target field or property.)
		/// </summary>
		public Arity Arity { get; protected set; }

		/// <summary>
		/// Gets the full name of the parameter (including its group name, if any).
		/// </summary>
		public string FullName
		{
			get
			{
				if (string.IsNullOrEmpty(this.ParamGroup.GroupName))
					return this.Name;
				else return this.ParamGroup.GroupName + "." + this.Name;
			}
		}

		/// <summary>
		/// Gets the type for the value(s) of this parameter, or null if it takes no values.
		/// If the target field or property is a List, then ValueType is the type of
		/// the list members, not the entire list.
		/// If the target field or property is INullable, then ValueType is the type
		/// wrapped by the INullable.
		/// </summary>
		public Type EffectiveValueType { get; private set; }	// only SetValueTypes() is allowed to set this
		//{
		//    get { return this._valueType; }
		//    protected set
		//    {
		//        this._valueType = value;
		//        if (value == null)
		//            return;
		//        Require.Condition(!value.HasElementType, "use the member type, not the array or collection type");
		//        Require.Condition(!value.IsNullable(), "for nullable types, use the inner type");
		//    }
		//}
		//private Type _valueType;	// this should only be touched by the property's getter and setter

		/// <summary>
		/// Gets the actual declared type of the target field or property.
		/// </summary>
		public Type ActualValueType { get; private set; }	// only SetValueTypes() is allowed to set this
		//{
		//    get { return this._actualValueType; }
		//}
		//private Type _actualValueType;	// this should only be touched by the property's getter and setter

		/// <summary>
		/// Gets whether the parameter been found in the parsed command-line parameters.
		/// </summary>
		public bool Found
		{
			get { return this._found; }
			internal set
			{
				Require.Condition(value, "Why are you trying to un-find a parameter?");
				if (this._found)
					throw new CmdLineSyntaxError(this.Name + " specified more than once");
				this._found = value;
			}
		}
		private bool _found = false;

		/// <summary>
		/// Gets whether the parameter currently has any value at all (regardless 
		/// of whether any command-line arguments have been parsed yet).
		/// </summary>
		/// <remarks>
		/// If ActualValueType is System.Nullable&lt;&gt;, then this property is equal to
		///		that of the HasValue property of the value.
		/// If ActualValueType is a (non-nullable) scalar value type 
		///		(int, bool, enums, etc.), then this property is always true.
		/// If ActualValue is a reference type and the value is null, then this property is null.
		/// If ActualValueType is a list or array that is either null or empty, 
		///		this property will be false; if the list or array contains one
		///		or more elements, then this property will be true.
		/// </remarks>
		public abstract bool HasValue { get; }

		#endregion

		private const int MaxParamNameLength = 42;	// arbitrary

		/// <summary> our parent (never null)  </summary>
		protected readonly CmdLineParamGroup ParamGroup;

		// shortcut
		/// <summary> shortcut  </summary>
		protected CmdLineParser CmdLineParser { get { return this.ParamGroup.CmdLineParser; } }

		internal abstract void SetValue (object value);
		internal abstract string GetValueString ();

		//-----------------------------------------------------------------------------
		/// <summary> Constructor </summary>
		/// <param name="name">the name of the parameter</param>
		/// <param name="group">the group to which the new parameter belongs</param>
		/// <param name="valueType">the type of the parameter's value</param>

		protected CmdLineParam (string name, CmdLineParamGroup group, Type valueType)
		{
			Require.NotNull(group);
			this.Name = name;
			this.ParamGroup = group;
			this.EffectiveValueType = valueType;
		}

		//-----------------------------------------------------------------------------
		/// <summary> Constructor </summary>
		/// <param name="group">the group to which the new parameter belongs</param>
		/// <param name="clpAttr">the custom attribute on the member, or null</param>
		/// <param name="memberInfo">the memberInfo for the member, or null</param>

		protected CmdLineParam (CmdLineParamGroup group, CmdLineArgAttribute clpAttr,
																MemberInfo memberInfo)
		{
			Require.NotNull(group);
			this.ParamGroup = group;

			if (clpAttr != null)
			{
				this.Name = clpAttr.Name;
				this.Position = clpAttr.Position;
				this.IsRequired = clpAttr.Required;
				this.Help = clpAttr.Help;
			}
			else this.IsRequired = false;

			if (string.IsNullOrEmpty(this.Name))
				this.Name = memberInfo.Name;

			string errors = GetParamNameErrors(this.Name, group.CmdLineParser.SwitchChar);
			if (errors != null)
				throw new CmdLineSyntaxError("invalid parameter name " + 
								ErrorUtils.ValueToString(this.Name) + ": " + errors);

#if !SILVERLIGHT	// Silverlight lacks SettingsDescriptionAttribute
			if (string.IsNullOrEmpty(this.Help))
			{
				ListString descriptions = new ListString("\n");
				foreach (SettingsDescriptionAttribute sda in 
					memberInfo.GetCustomAttributes(typeof(SettingsDescriptionAttribute), false))
				{
					if (!string.IsNullOrEmpty(sda.Description))
						descriptions.Add(sda.Description);
				}
				this.Help = descriptions.ToString();
			}
#endif

		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Sets the values of the properties ActualType and EffectiveType.
		/// </summary>

		protected void SetValueType (Type actualType, Type effectiveType)
		{
			Require.Condition((actualType == null) == (effectiveType == null), "both args must be null or non-null");
			Require.Condition(this.ActualValueType == null, "type already set");

			Require.Condition(!effectiveType.HasElementType, "use the member type, not the array or collection type");
			Require.Condition(!effectiveType.IsNullable(), "for nullable types, use the inner type");

			// this is the only place where these properties get assigned
			this.ActualValueType = actualType;
			this.EffectiveValueType = effectiveType;
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Sets the values of the properties ActualType and EffectiveType.
		/// </summary>

		protected void SetValueType (Type actualAndEffectiveType)
		{
			this.SetValueType(actualAndEffectiveType, actualAndEffectiveType);
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Returns a value parser to be used with this command-line parameter.
		/// </summary>

		protected ICmdLineValueParser GetValueParser (CmdLineArgAttribute clpAttr, 
												MemberInfo memberInfo, Type valueType)
		{
			// See if our parent group (or its parent CmdLineParser) has one.

			ICmdLineValueParser parser = this.ParamGroup.GetValueParser(valueType);
			if (parser != null)
				return parser;

			throw new Exception("can't find or create a value parser for type " + valueType.FullName);
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Parses the given string into a value for this command-line parameter.
		/// Throws an exception if value is bogus.
		/// </summary>

		protected object ParseValue (string input)
		{
			try
			{
				return this.ValueParser.Parse(input, this.ToString());
			//	return this.ValueParser.Invoke(null, new object[] { input });
			}
			catch (CmdLineSyntaxError)
			{
				throw;
			}
			catch (Exception x)
			{
				string msg = "\"" + input + "\" is not a valid value for " + 
											this.FullName + " : " + x.Message;
				throw new CmdLineSyntaxError(msg, x);
			}
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Returns a string containing one or more error messages regarding the 
		/// given parameter name, or null if there were no errors.  Checks only 
		/// for syntactic validity, not whether the name has been defined.
		/// </summary>
		/// <param name="name">the parameter name</param>
		/// <param name="switchChar">the switch character</param>
		/// <returns>error messages or null</returns>
		/// <remarks>
		/// This is (or should be) the only place where the name validation rules
		/// are implemented.
		/// </remarks>

		internal static string GetParamNameErrors (string name, char switchChar)
		{
			ListString errors = new ListString("; ");

			if (name.Length > MaxParamNameLength)
				errors.Add("name must not exceed " + MaxParamNameLength.ToString() + " characters");

			// switch character can appear only at the beginning of a name

			int n = 0;
			for (; n < name.Length; n++)
				if (name[n] != switchChar)
					break;

			ListString badChars = new ListString(", ");

			for ( ; n < name.Length; n++)
				if (name[n] != '_' && !char.IsLetterOrDigit(name[n]))
					badChars.Add(ErrorUtils.ValueToString(name[n]));

			if (badChars.Count > 1)
				errors.Add(badChars.ToString() + " are not valid name characters");
			else if (badChars.Count == 1)
				errors.Add(badChars.ToString() + " is not a valid name character");

			double junk;
			if (double.TryParse(name, out junk))
				errors.Add("name must not represent a valid number");

			return errors.Count == 0 ? null : errors.ToString();
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Processes one ore more command-line args.
		/// </summary>
		/// <param name="actualName">
		///		The name of the parameter as actually given on the command line.
		///	</param>
		/// <param name="argValues">
		/// On entry, argValues[0] is the parameter name.
		/// On exit, argValues[0] is the first value (if any) that ProcessArgValues did not process.
		/// </param>
		/// <returns>the number of arg values processed by this call</returns>

		internal virtual int ProcessArgs (string actualName, ArgValueList argValues)
		{
			this.Found = true;

			if (this.Arity == Arity.None)
			{
				this.SetValue(true);
				return 0;
			}

			if (argValues.Remaining == 0)
				throw new CmdLineSyntaxError("expecting value after \"" + actualName + " = \"");

		//	Require.Condition(argValues.Remaining > 0);	// caller should have checked


			//if (argValues[0] == "=")
			//{
			//    argValues.Next();
			//    if (argValues.Remaining == 0)
			//        throw new CmdLineSyntaxError("expecting value after \"" + actualName + " = \"");
			//    return 2 + this.ProcessArgValues(actualName, argValues);
			//}

			//int eq = argValues[0].IndexOf('=');
			//if (eq < 0)
				return 1 + this.ProcessArgValues(actualName, argValues);
			//else
			//{
			//    argValues.Next();	// eat the parameter name & value
			//    string[] split = argValues[-1].Substring(eq+1).Split(Comma);
			//    ArgValueList embeddedArgValues = new ArgValueList(split);
			//    return 1 + this.ProcessArgValues(actualName, embeddedArgValues);
			//}
		}

		private static readonly char[] Comma = { ',' };

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Processes one or more positional command-line args.
		/// </summary>
		/// <param name="argValues">
		/// On entry, argValues[0] is the first unprocessed arg.
		/// On exit, argValues[0] is the first value (if any) that ProcessArgValues did not process.
		/// </param>
		/// <returns>the number of arg values processed by this call</returns>

		internal int ProcessArgs (ArgValueList argValues)
		{
			Require.Condition(argValues.Remaining > 0);	// caller should have checked
			this.Found = true;
			return this.ProcessArgValues(null, argValues);
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Processes zero or more command-line args as parameter values.
		/// </summary>
		/// <param name="actualName">
		///		The name of the parameter as actually given on the command line,
		///		or null if it was a positional parameter.
		///	</param>
		/// <param name="argValues">
		/// On entry, argValues[0] is the first value that ProcessArgValues should process.
		/// On exit, argValues[0] is the first value (if any) that ProcessArgValues did not process.
		/// </param>
		/// <returns>the number of arg values processed by this call</returns>

		private int ProcessArgValues (string actualName, ArgValueList argValues)
		{
			if (this.Arity == Arity.None)
				return 0;

			if (this.Arity == Arity.Single)
			{
				if (argValues.Remaining == 0)
					throw new CmdLineSyntaxError("missing value after \"" + actualName + "\"");
				this.SetValue(this.ParseValue(argValues[0]));
				argValues.Next();
				return 1;
			}
			else if (this.Arity == Arity.Multiple)
			{
				int n = 0;
				while (this.LooksLikeParamValue(argValues, 0))
				{
					// in this case, SetValue appends the new value to the appropriate collection
					this.SetValue(this.ParseValue(argValues[0]));
					argValues.Next();
					n++;
				}
				return n;
			}
			else Assert.NoCaseFor(this.Arity);
			return -1;	// shut compiler up
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Returns whether args[0] appears to be a parameter or switch value.
		/// </summary>

		internal bool LooksLikeParamValue (ArgValueList argValues, int n)
		{
			if (n >= argValues.Remaining)
				return false;

			// see if '=' is in arg[n], but not at start or end
			int e = argValues[n].IndexOf('=');
			if (e > 0 && e < (argValues[n].Length - 1))
				return false;

			// see if args[n] looks like a switch
			if (argValues[n].StartsWith(this.CmdLineParser.SwitchChar.ToString()))
				return false;

			// see args[n+1] is "="
			n++;
			if (n < argValues.Remaining && argValues[n] == "=")
				return false;

			return true;
		}

		//-----------------------------------------------------------------------------

		private static readonly Type[] ParseMethodArgTypes = { typeof(string) };

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Logs the parameter and its value.
		/// </summary>

		public virtual void Log (System.IO.TextWriter tw, string linePrefix=null)
		{
			if (linePrefix == null)
				linePrefix = "";

			tw.Write(linePrefix + this.FullName);

			if (this.Arity == Arity.None)
				tw.WriteLine(this.Found ? "  (provided)" : "  (not provided)");
			else if (this.Found)
				tw.WriteLine(" = " + this.GetValueString());
			else tw.WriteLine("  (not provided; default = " + this.GetValueString() + ")");
		}

		//-----------------------------------------------------------------------------
		/// for error reporting

		public override string ToString ( )
		{
			return "parameter \"" + this.Name + "\"";
		}
	}

}
